/*  Novelties: A visual novel programming language based on JavaFX
    Copyright (C) 2008  Hao Xu
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
 
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * /
 /*
 * DefaultScene.fx
 *
 * Created on Apr 9, 2008, 1:24:08 PM
 */

package avgengine.script;
import avgengine.*;
import avgengine.pred.*;
import javafx.ui.canvas.*;
import javafx.ui.*;
import java.lang.Thread;
import java.lang.System;
import java.io.Reader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.HashMap;
import java.util.Map;
/**
 * @author ertri
 */

class VNFX {
    attribute st: StreamTokenizer;
    attribute aliasMap: Map;
    attribute actorMap: Map;
    attribute posMap : Map;
    attribute sceneMap: Map;
    attribute r : Reader;
    attribute start: String;
    attribute prevAction: String;
    attribute prevActor: String;
    attribute choice_num: int;
    attribute choice_base: String;
    attribute guarded_num: int;
    attribute guarded_base: String;
    attribute clauseMap:ClauseMap;
    attribute env: Substitution;
    
    operation initialize();
    
    operation script();
    
    operation preface(): boolean;
    
    operation intro(): boolean;
    operation prologue(): boolean;
    operation rule(): boolean;
    
    operation scene():boolean;
    
    operation actor(): boolean;
    
    operation exp(a:Actor): boolean;
    
    operation map(): boolean;
    
    operation bg() :boolean;
    operation style(p:GenericParagraph):boolean;
    operation stage():boolean;
    operation paragraph(scene:ScriptedScene, b:Branch):boolean;
    operation literal(): Literal;
    operation term(): Term;
    
    operation  alias(): boolean;
    operation branch():boolean;
    operation side():int;
    
    function  isAlias(a:String , b:String): boolean;
    operation nextSVal():String;
    operation nextNVal():Number;
    function getActionName(a:String):String;
    function getActorName(a:String):String;
    
    
}
attribute VNFX.start = null;
attribute VNFX.env= Substitution{};

operation VNFX.initialize() {
    println("{r}");
    st = new StreamTokenizer(r);
    var cc = '%'.codePointAt(0);
    st.commentChar(cc);
    aliasMap = new HashMap();
    sceneMap = new HashMap();
    actorMap = new HashMap();
    posMap = new HashMap();
    clauseMap = ClauseMap{vnfx:this};
    
    
}
operation VNFX.script() {
    while (preface() or intro() or prologue() or scene()) {
        
    }
    return false;
}

operation  VNFX.preface(): boolean {
    return alias();
}
operation  VNFX.intro(): boolean {
    return actor() or map() or bg() or stage();
}
operation VNFX.prologue(): boolean {
    println("start of prologue.");
    return rule();
}

operation VNFX.alias():boolean {
    if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (isAlias(st.sval, "alias")) {
            st.nextToken();
            var alias = st.sval;
            st.nextToken();
            var name = st.sval;
            aliasMap.put(alias, name);
            return true;
        }
    }
    st.pushBack();
    return false;
    
}
operation VNFX.actor():boolean {
    if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (isAlias(st.sval, "actor")) {
            st.nextToken();
            var name = st.sval;
            var actor = Actor{
                expression: [],
                images: [],
                mapImages: [],
                music: []
            };
            actorMap.put(name, actor);
            while(exp(actor)){}
            actor.style = Style{};
            while(style(actor.style)){}
            return true;
        }
    }
    st.pushBack();
    return false;
    
}

operation VNFX.exp(a) {
    if(isAlias(nextSVal(), "exp" ) ) { /* use isAlias instead of '=='; to support internationalization */
        insert nextSVal() into a.expression;
        if(isAlias(nextSVal(),"image")) {
            insert Image{url:nextSVal()} into a.images;
        }else {
            st.pushBack();
            insert null into a.images;
        }
        if(isAlias(nextSVal(),"sprite")) {
            insert Image{url:nextSVal()} into a.mapImages;
        }else {
            st.pushBack();
            insert null into a.mapImages;
        }
        if(isAlias(nextSVal(),"music")) {
            insert MusicPlayer{url:nextSVal()} into a.music;
        }else {
            st.pushBack();
            insert null into a.images;
        }
        return true;
    }
    st.pushBack();
    return false;
    
}
operation VNFX.style(p:GenericParagraph):boolean {
    if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (isAlias(st.sval, "color")) {
            p.color = Color{red:nextNVal(),green:nextNVal(),blue:nextNVal(),opacity:nextNVal()};
            return true;
        } else if(isAlias(st.sval,"border")) {
            p.borderColor = Color{red:nextNVal(),green:nextNVal(),blue:nextNVal(),opacity: nextNVal()};
            p.borderWidth = nextNVal();
            return true;
        } else if (isAlias(st.sval,"font")) {
            p.font = Font{faceName: nextSVal(), size: nextNVal()};
            return true;
        }
    }
    st.pushBack();
    return false;
    
}

operation VNFX.map():boolean {
    if(isAlias(nextSVal(),"map")) {
        var name = nextSVal();
        var gw = nextNVal().intValue();
        var gh = nextNVal().intValue();
        var map = <<avgengine.map.Map>> {
            
            expression: [],
            images: [],
            music: []
        };
        map.gridWidth = gw;
        map.gridHeight = gh;
        actorMap.put(name, map);
        while(exp(map)) {}
        return true;
    }
    st.pushBack();
    return false;
}
operation VNFX.bg():boolean  {
    if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (isAlias(st.sval, "bg")) {
            st.nextToken();
            var name = st.sval;
            var actor = Actor{
                expression: [],
                images: [],
                music: []
            };
            actorMap.put(name, actor);
            while(exp(actor)){}
            return true;
        }
    }
    st.pushBack();
    return false;
    
}

operation VNFX.side():int {
    var t = this.nextSVal();
    if(t<>null){
        if(isAlias(t,"left")) {
            return LEFT:int;
        } else if(isAlias(t,"right")) {
            return RIGHT:int;
        }
    }
    st.pushBack();
    return -1;
}
operation VNFX.stage():boolean  {
    if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (isAlias(st.sval, "anchor")) {
            st.nextToken();
            var name = st.sval;
            st.nextToken();
            var pos = (int)st.nval;
            posMap.put(name, Position{name:name, x:pos});
            return true;
        }
    }
    st.pushBack();
    return false;
    
}

operation VNFX.scene():boolean {
    if(st.nextToken()==StreamTokenizer.TT_WORD) {
        if(isAlias(st.sval, "scene")) {
            st.nextToken();
            var name = st.sval;
            st.nextToken();
            //bg
            var bgname = nextSVal();
            var bg = (Actor)actorMap.get(bgname);
            var initExp = nextSVal();
            var anames = [bgname];
            var actors = [bg];
            var map;
            if(isAlias(nextSVal(), "map")) {
                var mapName = nextSVal();
                map = (<<avgengine.map.Map>>)actorMap.get(mapName);
                insert mapName into anames;
                insert map into actors;
            } else {
                st.pushBack();
            }
            while(isAlias(nextSVal(), "actor")) {
                st.nextToken();
                insert st.sval into anames;
                insert(Actor)actorMap.get(st.sval) into actors;
            }
            st.pushBack();
            var s = Style{};
            while(style(s)){}
            this.nextSVal();
            //start
            var branch = Branch {
                script:[]
            };
            var scene = DefaultScene {
                bg: bg
                map : map
                initExp : initExp
                actorNames: anames
                actors: actors
                script: branch
                defaultStyle : s
            };
            var itr = posMap.values().iterator();
            while( itr.hasNext()) {
                insert(Position)itr.next() into scene.pos;
            }
            while(paragraph(scene,branch)){}
            st.nextToken();
            //end
            if(start == null) {
                start = name;
            }
            this.sceneMap.put(name, scene);
            return true;
        }
    }
    st.pushBack();
    return false;
}
operation VNFX.getActionName(s:String) {
    if(s=="=") {
        return prevAction;
    } else {
        prevAction = s;
        return s;
    }
}

operation VNFX.getActorName(s:String) {
    println("VNFX.getActorName: {s}");
    if(s=="=") {
        return prevActor;
    } else {
        prevActor = s;
        println("PrevActor = {s}");
        return s;
    }
}

attribute VNFX.choice_base="@choice";
attribute VNFX.choice_num = 0;
attribute VNFX.guarded_base="@guarded";
attribute VNFX.guarded_num = 0;

operation VNFX.paragraph(s, b) {
    var stsval = getActionName(this.nextSVal());
    if(isAlias(stsval,"enter")) {
        var name = getActorName(this.nextSVal());
        var exp = this.nextSVal();
        var side = this.side();
        var pos = this.nextSVal();
        insert ActorEnters{actorName: name,exp: exp,side: side,anchor: pos} into b.script;
        return true;
    } else  if(isAlias(stsval,"reenter")) {
        var name = getActorName(this.nextSVal());
        var side = this.side();
        var pos = this.nextSVal();
        insert ActorReenters{actorName: name,side: side,anchor: pos} into b.script;
        return true;
    } else if(isAlias(stsval,"exit")) {
        var name = getActorName(this.nextSVal());
        var side = this.side();
        insert ActorExits{actorName: name,side: side} into b.script;
        return true;
    } else if(isAlias(stsval,"show")) {
        var name = getActorName(this.nextSVal());
        var exp = null ;
        var x = term();
        var y = term();
        insert ActorShowsOnMap{actorName: name,x: x,y: y, vnfx:this} into b.script;
        return true;
    } else if(isAlias(stsval,"translate")) {
        var name = getActorName(this.nextSVal());
        var exp = null;
        var x = term();
        var y = term();
        insert ActorMovesOnMap{actorName: name,x: x,y: y, vnfx:this} into b.script;
        return true;
    } else if(isAlias(stsval,"hide")) {
        var name = getActorName(this.nextSVal());
        var exp = this.nextSVal();
        insert ActorHidesOnMap{actorName: name} into b.script;
        return true;
    } else if(isAlias(stsval,"say")) {
        var name = getActorName(this.nextSVal());
        println("Actor name = {name}");
        var pstring = this.nextSVal();
        var sync = this.nextSVal();
        var async = true;
        if(sync=="sync") {
            async=false;
        } else {
            st.pushBack();
        }
        insert ActorSays{actorName: name,pstring:pstring,asynchronous: async} into b.script;
        return true;
    } else if(isAlias(stsval,"change")) {
        var name = getActorName(this.nextSVal());
        var exp = this.nextSVal();
        insert ActorChanges{actorName: name,exp:exp} into b.script;
        return true;
    }else if(isAlias(stsval,"move") ) {
        var name = getActorName(this.nextSVal());
        var pos = this.nextSVal();
        insert ActorMoves{actorName: name,anchor: pos} into b.script;
        return true;
    } else if(isAlias(stsval,"choose" ) ){
        var action = Choose{choices:[],branchNames:[]};
        while(isAlias(this.nextSVal(),"choice")) {
            insert nextSVal() into action.choices;
            var branchName = "{choice_base}{choice_num++}";
            insert branchName into action.branchNames;
            var branch = Branch{name: branchName};
            while(paragraph(s, branch)) {} // assume that paragraph does not recognize keyword choice
            insert branch into s.branches;
        }
        //don't pushback, need to consume keyword end st.pushBack();
        insert action into b.script;
        return true;
    } else if(isAlias(stsval, "wait") ){
        var nsec = (int)this.nextNVal();
        insert Wait{msec: nsec} into b.script;
        return true;
    } else if(isAlias(stsval, "branch") ){
        var name = this.nextSVal();
        var branch = Branch{name: name};
        this.nextSVal();
        // start
        while(paragraph(s, branch)) {}
        this.nextSVal();
        // end
        insert branch into s.branches;
        return true;
    } else if(isAlias(stsval, "go")) {
        var name = this.nextSVal();
        insert Jump{name: name} into b.script;
        return true;
    } else if (isAlias(stsval, "skip")) {
        return true;
    }else if(isAlias(stsval, "if") ){
        var guard = literal();
        nextSVal();
        // then
        var branchName = "{guarded_base}{guarded_num++}";
        var branch = Branch{name: branchName};
        while(paragraph(s, branch)) {} // assume that paragraph does not recognize keyword end
        insert branch into s.branches;
        nextSVal();
        // end
        
        var ga = GuardedAction{guard: guard, action: Jump{name: branchName}, env: env, clauseMap: clauseMap};
        insert ga into b.script;
        return true;
    } else if (isAlias(stsval, "set")) {
        var v = (Variable)term();
        var t = term();
        insert new SetAction{
            variable:v,
            term:t,
            env:env,
            vnfx:this
        } into b.script;
        return true;
    }
    
    
    st.pushBack();
    return false;
    
}
operation VNFX.rule() {
    var sval = this.nextSVal();
    if(isAlias(sval,"rule")) {
        var head = literal();
        nextSVal();
        //if
        var c  = Clause{head: head};
        var l : Literal=literal();
        while(l<>null) {
            insert l into c.tail;
            l = literal();
        }
        nextSVal();
        //end
        clauseMap.addClause(c);
        return true;
    } else if(isAlias(sval,"fact")) {
        var head = literal();
        clauseMap.addClause(Clause{head: head, tail: []});
        return true;
        
    }
    st.pushBack();
    return false;
}
operation VNFX.literal() {
    var comp = Computible{};
    var dic = Dictionary{};
    var p = this.nextSVal();
    if(p<>";" and not isAlias(p,"if") and not isAlias(p,"end")) {
        if(nextSVal() =="(") {
            var params:Term* = [];
            var t = term();
            while(t<>null) {
                
                insert t into params;
                t = term();
            }
            nextSVal();
            //)
            var pred = dic.lookupPredicate(Predicate{n:p});
            return Literal{
                p:pred,
                params:params,
                sign:1
            };
        } else  {
            st.pushBack();
            return Proposition{n:p};
            
        }
    }
    st.pushBack();
    return null;
    
}
operation VNFX.term() {
    var dic = Dictionary{};
    var p = this.nextSVal();
    if(st.ttype == st.TT_NUMBER) {
        return FunctionTerm{f:Nat{num:st.nval.intValue()}};
    }
    if(p<>")") {
        if(nextSVal() =="(") {
            var params:Term*;
            var t = term();
            while(t<>null) {
                
                insert t into params;
                t = term();
            }
            nextSVal();
            //)
            return FunctionTerm{f:dic.lookupFunction(Function{n:p}),params:params};
        } else  {
            st.pushBack();
            return Variable{n:p};
            
        }
    }
    st.pushBack();
    return null;
    
}
function VNFX.isAlias(a:String , b:String): boolean {
    
    return if aliasMap.containsKey(a) then b.equals(aliasMap.get(a)) else a.equals(b);
}

operation VNFX.nextSVal() {
    st.nextToken();
    if(st.ttype<>st.TT_NUMBER and st.ttype<>st.TT_WORD
    and st.ttype<>"\"".codePointAt(0) and st.ttype<>"'".codePointAt(0)
    and st.ttype<>st.TT_EOF and st.ttype <> st.TT_EOL) {
        var ch = new String(st.ttype);
        return ch;
    }
    return st.sval;
}

operation VNFX.nextNVal() {
    st.nextToken();
    return st.nval;
}



/* DefaultScene */
class DefaultScene extends ScriptedScene {
    attribute buttonHighlight:Dialog;
    attribute initExp: String;
}
operation DefaultScene.initialize() {
    if(init) {
        return;
    }
    stage = Stage{dwidth: 800, dheight: 600};
    
    dialog = Dialog{
        dwidth :750,
        dheight:150,
        x:25,
        y:425,
        pad:[10,10],
        dopacity: .5,
        background: green
    };
    control = Control{
        dwidth: 50
        dheight: 150
        x: 725
        y: 425
        pad:[9,9]
        dopacity:.25
        borderWidth: 0
        arc: 0
        background: blue
    };
    buttonHighlight = Dialog{
        opacity:0.3,
        dwidth:34,
        dheight:34,
        pad:[1,1],
        background:blue,
        borderWidth:0,
        borderColor:blue,
        arc:0,
        speed : 10
        
    };
    click = ButtonParagraph {
        topacity:1
        image:Image{url:"click.bmp"}
        highlight:buttonHighlight
    };
    var exit = ButtonParagraph {
        height: 35
        topacity:1
        image:Image{url:"exit.bmp"}
        highlight:buttonHighlight
        onMouseClicked : operation(e:CanvasMouseEvent) {
            if(e.button==1) {
                System.exit(0);
            }
        }
    };
    var s;
    if(defaultStyle<>null) {
        s = defaultStyle;
        
    } else {
        s = Style{};
    }
    defaultStyle= GenericParagraph {
        topacity: 1,
        color:white
        borderColor:white
        borderWidth:1
        font:Font{faceName:"Century Gothic", size: 30}
        height:30
    };
    defaultStyle.applyStyle(s);
    
    control.addButton(exit);
    super.initialize();
    this.metrics = new TextMetrics(4, dialog.dwidth - 2*dialog.pad[0]-control.dwidth);
}


operation addAllActors(vnfx:VNFX, startScene:ScriptedScene) {
    var iter = vnfx.actorMap.keySet().iterator();
    while(iter.hasNext()) {
        var key = iter.next();
        insert(String)key into startScene.actorNames;
        insert(Actor)vnfx.actorMap.get(key) into startScene.actors;
    }
}

operation playScene(canvas:Canvas, vnfx:VNFX, start:String) {
    var startScene = (DefaultScene)vnfx.sceneMap.get(start);
    startScene.initialize();
    canvas.content = startScene.sp;
    startScene.selectInitialBackground(startScene.initExp);
    Thread.sleep(100);
    startScene.runScript(
    operation(scene:String){
        playScene(canvas, vnfx, scene);
    });
}
operation play(n:String, cs: String) {
    var is = new FileInputStream(n);
    var reader = new InputStreamReader(is, cs);
    println("{reader}");
    var vnfx = VNFX {r:reader};
    vnfx.initialize();
    vnfx.script();
    println("{vnfx.actorMap}");
    println("{vnfx.posMap}");
    println("{vnfx.aliasMap}");
    println("{vnfx.sceneMap}");
    println("{vnfx.start}");
    var tk = PredToolkit{};
    for(cls in vnfx.clauseMap.clauses) {
        println(tk.printClause(cls));
        //        for(b in s.branches) {
        //            println("branch: {b.name}, {b.script}");
        //        }
        
    }
    
    var itr = vnfx.sceneMap.values().iterator();
    while(itr.hasNext()) {
        var s = (DefaultScene)itr.next();
        println("main: {s.script}");
        for(b in s.branches) {
            println("branch: {b.name}, {b.script}");
        }
        
    }
    
    var canvas = Canvas {};
    var f = Frame {
        content: canvas
        visible: true
    };
    playScene(canvas, vnfx, vnfx.start);
    
}
